﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/PositionInt32.cs
//  *
//  ***************************************************************************

#endregion

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

using Haplous.Core;
using Haplous.Correctness;

using JetBrains.Annotations;

namespace Haplous
{
    /// <summary>
    ///     An extended implementation of <see cref="Position{T}" /> for <see cref="int" /> values. This class cannot be
    ///     inherited.
    /// </summary>
    [DebuggerDisplay("PositionInt32: {X},{Y}")]
    public sealed class PositionInt32 : Position<int>, IProperty<PositionInt32>
    {
        /// <summary>
        ///     Initializes a new instance of the <see cref="Position{T}" /> class.
        /// </summary>
        /// <param name="x">The horizontal coordinate measured in pixels from the left border.</param>
        /// <param name="y">The vertical coordinate measured in pixels from the top border.</param>
        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x")]
        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y")]
        public PositionInt32(int x, int y) : base(x, y)
        {}

        /// <summary>
        ///     Initializes a new instance of the <see cref="Position{T}" /> class.
        /// </summary>
        public PositionInt32()
        {}

        /// <summary>
        ///     Copies the value of the specified property object into the current property object.
        /// </summary>
        /// <param name="property">The property object from which to copy the value.</param>
        public void Copy(IProperty<PositionInt32> property)
        {
            var position = (PositionInt32)property;
            WaitForDirectorUpdate();
            Set(position);
        }

        /// <summary>
        ///     Adds the specified values to the <see cref="Position{T}.X" /> and <see cref="Position{T}.Y" /> coordinates.
        /// </summary>
        /// <param name="dx">The amount by which to change the horizontal coordinate.</param>
        /// <param name="dy">The amount by which to change the vertical coordinate.</param>
        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dy")]
        public void Add(int dx, int dy)
        {
            Set(X + dx, Y + dy);
        }

        /// <summary>
        ///     Adds the specified values to the <see cref="Position{T}.X" /> and <see cref="Position{T}.Y" /> coordinates.
        /// </summary>
        /// <param name="delta">A position containing the amounts by which to change the horizontal and vertical coordinates.</param>
        public void Add([NotNull] PositionInt32 delta)
        {
            delta.ValidateArgument("delta");
            Set(X + delta.X, Y + delta.Y);
        }

        /// <summary>
        ///     Subtracts the specified values to the <see cref="Position{T}.X" /> and <see cref="Position{T}.Y" /> coordinates.
        /// </summary>
        /// <param name="dx">The amount by which to change the horizontal coordinate.</param>
        /// <param name="dy">The amount by which to change the vertical coordinate.</param>
        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dy")]
        public void Subtract(int dx, int dy)
        {
            Set(X - dx, Y - dy);
        }

        /// <summary>
        ///     Subtracts the specified values to the <see cref="Position{T}.X" /> and <see cref="Position{T}.Y" /> coordinates.
        /// </summary>
        /// <param name="delta">A position containing the amounts by which to change the horizontal and vertical coordinates.</param>
        public void Subtract([NotNull] PositionInt32 delta)
        {
            delta.ValidateArgument("delta");
            Set(X - delta.X, Y - delta.Y);
        }

        /// <summary>
        ///     Implements the add operator.
        /// </summary>
        /// <param name="op1">The first operand.</param>
        /// <param name="op2">The second operand.</param>
        /// <returns>The result of the operation.</returns>
        [SuppressMessage("Microsoft.Design", "CA1013:OverloadOperatorEqualsOnOverloadingAddAndSubtract")]
        public static PositionInt32 operator +([NotNull] PositionInt32 op1, [NotNull] PositionInt32 op2)
        {
            op1.ValidateArgument("op1");
            op2.ValidateArgument("op2");

            var result = op1.Clone();
            result.Add(op2);
            return result;
        }

        /// <summary>
        ///     Implements the subtract operator.
        /// </summary>
        /// <param name="op1">The first operand.</param>
        /// <param name="op2">The second operand.</param>
        /// <returns>The result of the operation.</returns>
        [SuppressMessage("Microsoft.Design", "CA1013:OverloadOperatorEqualsOnOverloadingAddAndSubtract")]
        public static PositionInt32 operator -([NotNull] PositionInt32 op1, [NotNull] PositionInt32 op2)
        {
            op1.ValidateArgument("op1");
            op2.ValidateArgument("op2");

            var result = op1.Clone();
            result.Subtract(op2);
            return result;
        }

        /// <summary>
        ///     Clones this instance.
        /// </summary>
        /// <returns>A new instance set to the same position as this instance.</returns>
        /// <remarks>The locking state is not copied</remarks>
        [NotNull]
        public PositionInt32 Clone()
        {
            return new PositionInt32(X, Y);
        }

        /// <summary>
        ///     Determine whether the current position and the new one given overlap or have passed through one another.
        /// </summary>
        /// <param name="newSize">The new size.</param>
        /// <param name="otherPosition">The position of the item with which he collision has taken place.</param>
        /// <param name="otherSize">The size of the item with which he collision has taken place.</param>
        /// <param name="currentPosition">The current position.</param>
        /// <returns><see langword="true" /> if the items are overlapping, <see langword="false" /> otherwise.</returns>
        public bool IsOverlapping([NotNull] SizeInt32 newSize, [NotNull] PositionInt32 otherPosition, [NotNull] SizeInt32 otherSize, [NotNull] PositionInt32 currentPosition)
        {
            if(IsCurrentlyOverlapping(newSize, otherPosition, otherSize))
                return true;

            return HasPassedThrough(otherPosition, currentPosition);
        }

        /// <summary>
        ///     Determine whether the current position and the new one given overlap or have passed through one another.
        /// </summary>
        /// <param name="otherPosition">The position of the item with which he collision has taken place.</param>
        /// <param name="currentPosition">The current position.</param>
        /// <returns><see langword="true" /> if the items are overlapping, <see langword="false" /> otherwise.</returns>
        private bool HasPassedThrough([NotNull] PositionInt32 otherPosition, [NotNull] PositionInt32 currentPosition)
        {
            // Did they need to pass through each other to get to their new positions.
            if((currentPosition.X < otherPosition.X) && (X < otherPosition.X))
                return false; // The moving shape has not crossed from left to right

            if((currentPosition.X > otherPosition.X) && (X > otherPosition.X))
                return false; // The moving shape has not crossed from right to left

            if((currentPosition.Y < otherPosition.Y) && (Y < otherPosition.Y))
                return false; // The moving shape has not crossed from top to bottom

            if((currentPosition.Y > otherPosition.Y) && (Y > otherPosition.Y))
                return false; // The moving shape has not crossed from bottom to top

            return true;
        }

        /// <summary>
        ///     Determine whether the items given are currently overlapping.
        /// </summary>
        /// <param name="newSize">The new size.</param>
        /// <param name="otherPosition">The position of the item with which he collision has taken place.</param>
        /// <param name="otherSize">The size of the item with which he collision has taken place.</param>
        /// <returns><see langword="true" /> if the items are overlapping, <see langword="false" /> otherwise.</returns>
        private bool IsCurrentlyOverlapping([NotNull] SizeInt32 newSize, [NotNull] PositionInt32 otherPosition, [NotNull] SizeInt32 otherSize)
        {
            // Is the shape currently overlapping?
            if((otherPosition.X < X) && ((otherPosition.X + otherSize.Width) < X))
                return false; // The second shape is fully to the left of first shape.

            if((otherPosition.X > X) && (otherPosition.X > (X + newSize.Width)))
                return false; // The second shape is fully to the right of first shape.

            if((otherPosition.Y < Y) && ((otherPosition.Y + otherSize.Height) < Y))
                return false; // The second shape is fully above first shape.

            if((otherPosition.Y > Y) && (otherPosition.Y > (Y + newSize.Height)))
                return false; // The second shape is fully below first shape.
            return true;
        }

        /// <summary>
        ///     Corrects the horizontal part of an overlap where it isn't permitted.
        /// </summary>
        /// <param name="newSize">The new size.</param>
        /// <param name="otherPosition">The position of the item with which he collision has taken place.</param>
        /// <param name="otherSize">The size of the item with which he collision has taken place.</param>
        /// <param name="currentPosition">The current position.</param>
        /// <param name="currentSize">The current size.</param>
        /// <returns>The amount by which the horizontal coordinate must be adjusted.</returns>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        public int CorrectHorizontalOverlap([NotNull] SizeInt32 newSize, [NotNull] PositionInt32 otherPosition, [NotNull] SizeInt32 otherSize, [NotNull] PositionInt32 currentPosition,
                                            [NotNull] SizeInt32 currentSize)
        {
            newSize.ValidateArgument("newSize");
            otherPosition.ValidateArgument("otherPosition");
            otherSize.ValidateArgument("otherSize");
            currentPosition.ValidateArgument("currentPosition");
            currentSize.ValidateArgument("currentSize");

            var left = X;
            var otherLeft = otherPosition.X;
            var right = left + newSize.Width - 1;
            var otherRight = otherLeft + otherSize.Width - 1;

            // Left edge of moving object.
            int d;
            if(((left >= otherLeft) && (left <= otherRight)) || ((left > otherLeft) && (currentPosition.X <= otherLeft)))
            {
                // The left edge of the moving object will overlap with the impacted object or has passed across it.
                if(left > currentPosition.X)
                {
                    // We're moving right, so step back towards the left
                    d = (otherLeft - newSize.Width) - left;
                    return d;
                }

                // We were traveling left, so move to the right
                d = (otherRight + 1) - left;
                return d;
            }

            // Right edge of moving object
            if(((right >= otherLeft) && (right <= otherRight)) || ((left < otherLeft) && (currentPosition.X >= otherLeft)))
            {
                // The right edge of the moving object will overlap with the impacted object.
                if(left > currentPosition.X)
                {
                    // We're moving right, so step back towards the left
                    d = (otherLeft - newSize.Width) - left;
                    return d;
                }

                // We were traveling left, so move to the right
                d = (otherRight + 1) - left;
                return d;
            }

            return 0;
        }

        /// <summary>
        ///     Corrects the vertical part of an overlap where it isn't permitted.
        /// </summary>
        /// <param name="newSize">The new size.</param>
        /// <param name="otherPosition">The position of the item with which he collision has taken place.</param>
        /// <param name="otherSize">The size of the item with which he collision has taken place.</param>
        /// <param name="currentPosition">The current position.</param>
        /// <param name="currentSize">The current size.</param>
        /// <returns>The amount by which the horizontal coordinate must be adjusted.</returns>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        public int CorrectVerticalOverlap([NotNull] SizeInt32 newSize, [NotNull] PositionInt32 otherPosition, [NotNull] SizeInt32 otherSize, [NotNull] PositionInt32 currentPosition,
                                          [NotNull] SizeInt32 currentSize)
        {
            newSize.ValidateArgument("newSize");
            otherPosition.ValidateArgument("otherPosition");
            otherSize.ValidateArgument("otherSize");
            currentPosition.ValidateArgument("currentPosition");
            currentSize.ValidateArgument("currentSize");

            var top = Y;
            var otherTop = otherPosition.Y;
            var bottom = top + newSize.Height - 1;
            var otherBottom = otherTop + otherSize.Height - 1;

            // Top edge of moving object.
            int d;
            if(((top >= otherTop) && (top <= otherBottom)) || ((top > otherTop) && (currentPosition.Y <= otherTop)))
            {
                // The top edge of the moving object will overlap with the impacted object.
                if(top > currentPosition.Y)
                {
                    // We're moving down, so step back towards the top
                    d = (otherTop - newSize.Height) - top;
                    return d;
                }

                // We were traveling up, so move to the bottom
                d = (otherBottom + 1) - top;
                return d;
            }

            // Bottom edge of moving object
            if(((bottom >= otherTop) && (bottom <= otherBottom)) || ((top < otherTop) && (currentPosition.Y >= otherTop)))
            {
                // The bottom edge of the moving object will overlap with the impacted object.
                if(top > currentPosition.Y)
                {
                    // We're moving down, so step back towards the top
                    d = (otherTop - newSize.Height) - top;
                    return d;
                }

                // We were traveling up, so move to the bottom
                d = (otherBottom + 1) - top;
                return d;
            }

            return 0;
        }

        /// <summary>
        ///     Corrects an overlap where it isn't permitted.
        /// </summary>
        /// <param name="currentPosition">The current position of the solid that is being moved.</param>
        /// <param name="currentSize">The current size of the solid that is being moved.</param>
        /// <param name="newSize">The new size of the object.</param>
        /// <param name="otherPosition">The position of the "other" item with the which the collision will occur.</param>
        /// <param name="otherSize">The size of the "other" item with the which the collision will occur.</param>
        public void CorrectOverlap([NotNull] SizeInt32 newSize, [NotNull] PositionInt32 otherPosition, [NotNull] SizeInt32 otherSize, [NotNull] PositionInt32 currentPosition,
                                   [NotNull] SizeInt32 currentSize)
        {
            newSize.ValidateArgument("newSize");
            otherPosition.ValidateArgument("otherPosition");
            otherSize.ValidateArgument("otherSize");
            currentPosition.ValidateArgument("currentPosition");
            currentSize.ValidateArgument("currentSize");

            // We want to make the smallest correct possible.  So try fixing horizontal and vertical independently and
            // then use the smallest correction if only one is needed.

            var dx = CorrectHorizontalOverlap(newSize, otherPosition, otherSize, currentPosition, currentSize);
            var dy = CorrectVerticalOverlap(newSize, otherPosition, otherSize, currentPosition, currentSize);

            var xDirection = X - currentPosition.X;
            var yDirection = Y - currentPosition.Y;

            // If we are only overlapping on one axis then we need no correction
            if((dx == 0) || (dy == 0))
                return;

            // If we are overlapping on both then try the smallest correction first (but only if we have moved at all in that axis). 
            if((Math.Abs(dx) <= Math.Abs(dy)))
            {
                if(xDirection != 0)
                {
                    // X has the smallest change and it is moving in the X axis
                    Debug.Assert(Math.Abs(dx) <= Math.Abs(xDirection), "Correction cannot be greater than the original error.");
                    Set(X + dx, Y);
                }
                else
                {
                    if(yDirection != 0)
                    {
                        // Y does not have the smallest change, but we are moving in the Y axis.
                        Debug.Assert(Math.Abs(dy) <= Math.Abs(yDirection), "Correction cannot be greater than the original error.");
                        Set(X, Y + dy);
                    }
                    else
                    {
                        // We did not move in the X axis, but neither did we move in the Y(!?) axis and the change to X is smallest.
                        Debug.Assert(Math.Abs(dx) <= Math.Abs(xDirection), "Correction cannot be greater than the original error.");
                        Set(X + dx, Y);
                    }
                }
            }
            else
            {
                if(yDirection != 0)
                {
                    // Y has the smallest change and it is moving in the Y axis
                    Debug.Assert(Math.Abs(dy) <= Math.Abs(yDirection), "Correction cannot be greater than the original error.");
                    Set(X, Y + dy);
                }
                else
                {
                    if(xDirection != 0)
                    {
                        // X does not have the smallest change, but we are moving in the X axis.
                        Debug.Assert(Math.Abs(dx) <= Math.Abs(xDirection), "Correction cannot be greater than the original error.");
                        Set(X + dx, Y);
                    }
                    else
                    {
                        // We did not move in the Y axis, but neither did we move in the X(!?) axis and the change to Y is smallest.
                        Debug.Assert(Math.Abs(dy) <= Math.Abs(yDirection), "Correction cannot be greater than the original error.");
                        Set(X, Y + dy);
                    }
                }
            }

            // Now recalculate
            dx = CorrectHorizontalOverlap(newSize, otherPosition, otherSize, currentPosition, currentSize);
            dy = CorrectVerticalOverlap(newSize, otherPosition, otherSize, currentPosition, currentSize);

            // Again, if we are only overlapping on one axis then we need no correction
            if((dx == 0) || (dy == 0))
                return;

            // If not, then we'll need to apply both corrections
            Debug.Assert(Math.Abs(dx) <= Math.Abs(xDirection), "Correction cannot be greater than the original error.");
            Debug.Assert(Math.Abs(dy) <= Math.Abs(yDirection), "Correction cannot be greater than the original error.");
            Set(X + dx, Y + dy);
        }
    }
}